﻿using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

namespace XDL.Framework.Graphics
{
    /// <summary>
    /// Perlin-noise texture generator.
    /// </summary>
    /// <remarks>
    /// credits:
    /// http://freespace.virgin.net/hugo.elias/models/m_perlin.htm - Huge Elias
    /// http://shedletsky.com/jjshed/perlin/
    /// </remarks>
    public class PerlinTexture :
            Texture2D
    {
        delegate double InterpolateFunction(double a, double b, double x);
        InterpolateFunction SelectedInterpolateFunction;
        delegate double NoiseFunction(int x, int y);
        NoiseFunction SelectedNoiseFunction;
        delegate double NoiseBoundFunction(double n);
        NoiseBoundFunction SelectedNoiseBoundFunction;
        Random m_rnd = new Random();

        struct PerlinVarsStruct
        {
            public double ifreq;
            public double iamp;
            public double persistance;
            public int octaves;
            public int random_seed;
            public bool wrap;
            public int size_x;
            public int size_y;
            public double cur_freq; // used to manage wrap around texture generation, a bit of a hack
            public bool palette;
        };
        PerlinVarsStruct PerlinVars = new PerlinVarsStruct();

        /// <summary>
        /// Creates new land texture
        /// </summary>
        /// <param name="graphicsDevice">Graphic device</param>
        /// <param name="w">Width of the texture.</param>
        /// <param name="h">Height of the texture.</param>
        public PerlinTexture(GraphicsDevice graphicsDevice, int w, int h)
            :
            base(graphicsDevice, w, h, true, SurfaceFormat.Color)
        {
            SelectedInterpolateFunction = new InterpolateFunction(CubicInterpolate);
            SelectedNoiseFunction = new NoiseFunction(SmoothedNoise);
            SelectedNoiseBoundFunction = new NoiseBoundFunction(NormalizedTruncateBoundNoise);

            // default settings
            PerlinVars.iamp = 1;
            PerlinVars.ifreq = 0.0625;
            PerlinVars.octaves = 5;
            PerlinVars.persistance = 0.5;
            PerlinVars.random_seed = m_rnd.Next();
            PerlinVars.wrap = true;
            PerlinVars.palette = false;
            PerlinVars.size_x = w;
            PerlinVars.size_y = h;
        }

        public void SetPerlinVariables(double freq, double persistance)
        {
            PerlinVars.ifreq = freq;
            PerlinVars.persistance = persistance;
        }

        /// <summary>
        /// Generates the texture.
        /// </summary>
        public void Fill()
        {
            Color[] colors = new Color[this.Width * this.Height];
            for (int i = 0; i < colors.Length; i++)
            {
                int x = i % this.Height;
                int y = i / this.Width;
                colors[i] = ConvertNoiseToColor(SelectedNoiseBoundFunction(PerlinNoise(x, y)),x,y);
            }

            // copy the colors
            this.SetData<Color>(colors);
        }

        /// <summary>
        /// Convert the noise value into a color.
        /// </summary>
        /// <param name="noiseValue">The perlin noise value.</param>
        /// <param name="x">X (in texture pixel space)</param>
        /// <param name="y">Y (in texture pixel space)</param>
        /// <returns>A color.</returns>
        protected virtual Color ConvertNoiseToColor(double noiseValue,int x,int y)
        {
            byte noiseColor = (byte)(noiseValue * byte.MaxValue);
            return new Color(noiseColor, noiseColor, noiseColor);
        }

        private double AbsBoundNoise(double n)
        {
            if (n < 0.0) n = -n;
            if (n > 1.0) n = 1.0;
            return n;
        }

        private double TruncateBoundNoise(double n)
        {
            if (n < 0.0) n = 0.0;
            if (n > 1.0) n = 1.0;
            return n;
        }

        private double NormalizedTruncateBoundNoise(double n)
        {
            if (n < -1.0) n = -1.0;
            if (n > 1.0) n = 1.0;
            return (n * .5) + .5;
        }

        double Noise(int x)
        {
            x += PerlinVars.random_seed;
            x = (x << 13) ^ x;
            return (1.0 - ((x * (x * x * 15731 + 789221) + 1376312589) & 0x7fffffff) / 1073741824.0);
        }

        double Noise(int x, int y)
        {
            if (PerlinVars.wrap)
            {
                int s = (int)Math.Round((double)PerlinVars.size_x * PerlinVars.cur_freq);
                if (s > 0)
                {
                    x %= s;
                    y %= s;
                }
                if (x < 0) x += s;
                if (y < 0) y += s;
            }
            // all the mysterious large numbers in these functions are prime.
            return Noise(x + y * 8997587);//57);
        }

        double Noise(int x, int y, int z)
        {
            return Noise(x + (y * 89213) + (z * 8997587));
        }

        double SmoothedNoise(int x, int y)
        {
            double corners = (Noise(x - 1, y - 1) + Noise(x + 1, y - 1) + Noise(x - 1, y + 1) + Noise(x + 1, y + 1)) / 16;
            double sides = (Noise(x - 1, y) + Noise(x + 1, y) + Noise(x, y - 1) + Noise(x, y + 1)) / 8;
            double center = Noise(x, y) / 4;
            return corners + sides + center;
        }

        double InterpolatedNoise(double x, double y)
        {
            int a = (int)x;
            int b = (int)y;
            double frac_a = x - a;
            double frac_b = y - b;

            double v1 = SelectedNoiseFunction(a, b);
            double v2 = SelectedNoiseFunction(a + 1, b);
            double v3 = SelectedNoiseFunction(a, b + 1);
            double v4 = SelectedNoiseFunction(a + 1, b + 1);

            double i1 = SelectedInterpolateFunction(v1, v2, frac_a);
            double i2 = SelectedInterpolateFunction(v3, v4, frac_a);

            return SelectedInterpolateFunction(i1, i2, frac_b);
        }

        double LinearInterpolate(double a, double b, double x)
        {
            return a * (1 - x) + b * x;
        }

        double CosineInterpolate(double a, double b, double x)
        {
            double ft = x * 3.1415927;
            double f = (1 - Math.Cos(ft)) * .5;
            return a * (1 - f) + b * f;
        }

        double CubicInterpolate(double a, double b, double x)
        {
            double fac1 = 3 * Math.Pow(1 - x, 2) - 2 * Math.Pow(1 - x, 3);
            double fac2 = 3 * Math.Pow(x, 2) - 2 * Math.Pow(x, 3);

            return a * fac1 + b * fac2; //add the weighted factors

        }

        double NoInterpolation(double a, double b, double x)
        {
            return a;
        }

        double PerlinNoise(int x, int y)
        {
            double total = 0;
            double p = PerlinVars.persistance;
            int n = PerlinVars.octaves;

            double ifreq = PerlinVars.ifreq;
            double iamp = PerlinVars.iamp;

            double freq = ifreq;			// Math.Pow(2,i);
            double amp = iamp;				// Math.Pow(p,i);

            for (int i = 0; i < n; i++)
            {
                PerlinVars.cur_freq = freq;
                total += InterpolatedNoise((double)x * freq, (double)y * freq) * amp;
                freq *= 2;
                amp *= p;
            }
            return total;
        }
    }
}
